/**
 * 内部类
 * <p>
 * 内部类主要有如下作用。
 * ➢ 内部类提供了更好的封装，可以把内部类隐藏在外部类之内，不允许同一个包中的其他类访问该类。
 * 假设需要创建Cow类，Cow类需要组合一个CowLeg对象，CowLeg类只有在Cow类里才有效，离开了Cow类之后没有任何意义。
 * 在这种情况下，就可把CowLeg定义成Cow的内部类，不允许其他类访问CowLeg。
 * ➢ 内部类成员可以直接访问外部类的私有数据，因为内部类被当成其外部类成员，同一个类的成员之间可以互相访问。但
 * 外部类不能访问内部类的实现细节，例如内部类的成员变量。
 * ➢ 匿名内部类适合用于创建那些仅需要一次使用的类。
 * <p>
 * 对于前面介绍的命令模式，当需要传入一个Command对象时，重新专门定义PrintCommand和SquareCommand两个实现类可能没有太大的意义，
 * 因为这两个实现类可能仅需要使用一次。在这种情况下，使用匿名内部类将更方便。
 * <p>
 * 从语法角度来看，定义内部类与定义外部类的语法大致相同，内部类除需要定义在其他类里面之外，还存在如下两点区别。
 * ➢ 内部类比外部类可以多使用三个修饰符：private、protected、static—外部类不可以使用这三个修饰符。
 * ➢ 非静态内部类不能拥有静态成员。
 */

/**
 * 定义内部类非常简单，只要把一个类放在另一个类内部定义即可。
 * 此处的“类内部”包括类中的任何位置，甚至在方法中也可以定义内部类（方法里定义的内部类被称为局部内部类）。
 * 内部类定义语法格式如下：
 * <pre>
 *     public class OuterClass
 *     {
 *         // 此处可以定义内部类
 *     }
 * </pre>
 * 大部分时候，内部类都被作为成员内部类定义，而不是作为局部内部类。
 * 成员内部类分为两种：静态内部类和非静态内部类，使用static修饰的成员内部类是静态内部类，没有使用static修饰的成员内部类是非静态内部类。
 * 因为内部类作为其外部类的成员，所以可以使用任意访问控制符如private、protected和public等修饰。
 */
class Cow {
    private double weight;

    public Cow() {
    }

    public Cow(double weight) {
        this.weight = weight;
    }

    // 定义一个非静态内部类
    private class CowLeg {
        // 非静态内部类的两个实例变量
        private double length;
        private String color;

        // 非静态内部类的两个重载的构造器
        public CowLeg() {
        }

        public CowLeg(double length, String color) {
            this.length = length;
            this.color = color;
        }

        public double getLength() {
            return length;
        }

        public void setLength(double length) {
            this.length = length;
        }

        public String getColor() {
            return color;
        }

        public void setColor(String color) {
            this.color = color;
        }

        // 非静态内部类的实例方法
        public void info() {
            System.out.println("当前牛腿颜色是：" + color + ", 高：" + length);
            // 直接访问外部类的private修饰的成员变量
            System.out.println("本牛腿所在奶牛重：" + weight);
        }
    }

    public void testCowLeg() {
        CowLeg c1 = new CowLeg(1.12, "黑白相间");
        c1.info();
    }

    public static void test() {
        Cow cow = new Cow(378.9);
        cow.testCowLeg();
    }
}

/**
 * 前面提到过，在非静态内部类里可以直接访问外部类的private成员，
 * 这是因为在非静态内部类对象里，保存了一个它所寄生的外部类对象的引用（当调用非静态内部类的实例方法时，
 * 必须有一个非静态内部类实例，非静态内部类实例必须寄生在外部类实例里）。
 * 当在非静态内部类的方法内访问某个变量时，优先级：该方法内-》方法所在的内部类的成员变量-》内部类所在的外部类的成员变量-》编译错误
 * 因此，如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名，则可通过使用this、外部类类名.this作为限定来区分。
 */
class DiscernVariable {
    private String prop = "外部类的实例变量";

    private class InClass {
        private String prop = "内部类的实例变量";

        public void info() {
            String prop = "局部变量";
            // 通过外部类类名.this.varName访问外部类实例变量
            System.out.println("外部类的实例变量值：" + DiscernVariable.this.prop);
            // 通过this.varName访问内部类实例变量
            System.out.println("内部类实例变量：" + this.prop);
            // 直接访问局部变量
            System.out.println("局部变量的值：" + prop);
        }
    }

    public void testInClass() {
        InClass in = new InClass();
        in.info();
    }

    public static void test() {
        new DiscernVariable().testInClass();
    }
}

/**
 * 非静态内部类的成员可以访问外部类的实例成员，但反过来就不成立了。
 * 如果外部类需要访问非静态内部类的实例成员，则必须显式创建非静态内部类对象来调用访问其实例成员。
 * 非静态内部类对象必须寄生在外部类对象里，而外部类对象则不必一定有非静态内部类对象寄生其中。
 */

/**
 * 根据静态成员不能访问非静态成员的规则，外部类的静态方法、静态代码块不能访问非静态内部类，
 * 包括不能使用非静态内部类定义变量、创建实例等。
 * 总之，不允许在外部类的静态成员中直接使用非静态内部类。
 */
class StaticTest {
    // 定义一个非静态内部类，是一个空类
    private class In {
    }

    // 外部类的静态方法
    public static void test() {
        // 编译错误，因为非静态内部类的对象必须依赖外部类的对象存在
        // 而现在都没有外部类对象，也就没有办法创建非静态内部类的对象
//        new In();
    }
}

/**
 * Java不允许在非静态内部类里定义静态成员。有静态方法、静态成员变量、静态初始化块，
 */

/**
 * 静态内部类
 * 如果使用static来修饰一个内部类，则这个内部类就属于外部类本身，而不属于外部类的某个对象。
 * 因此使用static修饰的内部类被称为类内部类，有的地方也称为静态内部类。
 * <p>
 * 静态内部类可以包含静态成员，也可以包含非静态成员。
 * 根据静态成员不能访问非静态成员的规则，静态内部类不能访问外部类的实例成员，只能访问外部类的类成员。
 * 即使是静态内部类的实例方法也不能访问外部类的实例成员，只能访问外部类的静态成员。
 * <p>
 * 为什么静态内部类的实例方法也不能访问外部类的实例属性呢？
 * 因为静态内部类是外部类的类相关的，而不是外部类的对象相关的。
 * 也就是说，静态内部类对象不是寄生在外部类的实例中，而是寄生在外部类的类本身中。
 * 当静态内部类对象存在时，并不存在一个被它寄生的外部类对象，静态内部类对象只持有外部类的类引用，没有持有外部类对象的引用。
 * 如果允许静态内部类的实例方法访问外部类的实例成员，但找不到被寄生的外部类对象，这将引起错误。
 * <p>
 * 静态内部类是外部类的一个静态成员，因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等。
 * 外部类依然不能直接访问静态内部类的成员，但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员，
 * 也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。
 * 下面程序示范了这条规则。
 */
class AccessStaticInnerClass {
    static class StaticInnerClass {
        private static int prop1 = 5;
        private int prop2 = 9;
    }

    public void accessInnerProp() {
        // 只能通过类名访问静态内部类的类成员
//        System.out.println(prop1);
        System.out.println(StaticInnerClass.prop1);

        // 只能通过实例访问静态内部类的实例成员
//        System.out.println(prop2);
        System.out.println(new StaticInnerClass().prop2);
    }

    public static void test() {
        new AccessStaticInnerClass().accessInnerProp();
    }
}

/**
 * 除此之外，Java还允许在接口里定义内部类，接口里定义的内部类默认使用public static修饰，也就是说，接口内部类只能是静态内部类。
 * 如果为接口内部类指定访问控制符，则只能指定public访问控制符；
 * 如果定义接口内部类时省略访问控制符，则该内部类默认是public访问控制权限。
 * <p>
 * 接口里也可以定义内部接口
 * 接口里的内部接口是接口的成员，因此系统默认添加public static两个修饰符。
 * 如果定义接口里的内部接口时指定访问控制符，则只能使用public修饰符。
 */

/**
 * 使用内部类
 * 使用内部类定义变量和创建实例则与外部类存在一些小小的差异。下面分三种情况讨论内部类的用法。
 * 1.在外部类内部使用内部类
 * 从前面程序中可以看出，在外部类内部使用内部类时，与平常使用普通类没有太大的区别。
 * 一样可以直接通过内部类类名来定义变量，通过new调用内部类构造器来创建实例。
 * 唯一存在的一个区别是：不要在外部类的静态成员（包括静态方法和静态初始化块）中使用非静态内部类，因为静态成员不能访问非静态成员。
 * 在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。
 * <p>
 * 2.在外部类以外使用非静态内部类
 * 如果希望在外部类以外的地方访问内部类（包括静态和非静态两种），则内部类不能使用private访问控制权限，
 * private修饰的内部类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类，则能在访问控制符对应的访问权限内使用。
 * ➢ 省略访问控制符的内部类，只能被与外部类处于同一个包中的其他类所访问。
 * ➢ 使用protected修饰的内部类，可被与外部类处于同一个包中的其他类和外部类的子类所访问。
 * ➢ 使用public修饰的内部类，可以在任何地方被访问。
 * 在外部类以外的地方定义内部类（包括静态和非静态两种）变量的语法格式如下：
 * OuterClass.InnerClass varName;
 * <p>
 * 由于非静态内部类的对象必须寄生在外部类的对象里，因此创建非静态内部类对象之前，必须先创建其外部类对象。
 * 在外部类以外的地方创建非静态内部类实例的语法如下：
 * outerInstance.new InnerConstructor()
 * 在外部类以外的地方创建非静态内部类实例必须使用外部类实例和new来调用非静态内部类的构造器。
 */
class Out {
    // 定义一个内部类，不使用访问控制符。
    // 即只有同一个包中的其他类可以访问该内部类
    class In {
        public In(String msg) {
            System.out.println(msg);
        }
    }
}

class CreateInnerIntance {
    public static void test() {
        Out.In in = new Out().new In("测试信息");
        // 等同下面的3行
        Out.In in2;
        Out out = new Out();
        in2 = out.new In("测试信息");
    }
}

/**
 * 如果需要在外部类以外的地方创建非静态内部类的子类，则尤其要注意上面的规则：非静态内部类的构造器必须通过其外部类对象来调用。
 * <p>
 * 当创建一个子类时，子类构造器总会调用父类的构造器，
 * 因此在创建非静态内部类的子类时，必须保证让子类构造器可以调用非静态内部类的构造器，
 * 调用非静态内部类的构造器时，必须存在一个外部类对象。
 * 下面程序定义了一个子类继承了Out类的非静态内部类In类。
 */
class SubClass extends Out.In {
    // 显式定义SubClass的构造器
    public SubClass(Out out) {
        // 通过传入的Out对象显式调用In的构造器
        out.super("hello");
        // 非静态内部类In类的构造器必须使用外部类对象来调用，代码中super代表调用In类的构造器，而out则代表外部类对象
    }
}

/**
 * 从上面代码中可以看出，如果需要创建SubClass对象时，必须先创建一个Out对象。
 * 这是合理的，因为SubClass是非静态内部类In类的子类，非静态内部类In对象里必须有一个对Out对象的引用，
 * 其子类SubClass对象里也应该持有对Out对象的引用。
 * 当创建SubClass对象时传给该构造器的Out对象，就是SubClass对象里Out对象引用所指向的对象。
 * <p>
 * 非静态内部类In对象和SubClass对象都必须持有指向Outer对象的引用，
 * 区别是创建两种对象时传入Out对象的方式不同：
 * 当创建非静态内部类In类的对象时，必须通过Outer对象来调用new关键字；
 * 当创建SubClass类的对象时，必须使用Outer对象作为调用者来调用In类的构造器。
 * <p>
 * 注意：非静态内部类的子类不一定是内部类，它可以是一个外部类。
 * 但非静态内部类的子类实例一样需要保留一个引用，该引用指向其父类所在外部类的对象。
 * 也就是说，如果有一个内部类子类的对象存在，则一定存在与之对应的外部类对象。
 */

/**
 * 3.在外部类以外使用静态内部类
 * 因为静态内部类是外部类类相关的，因此创建静态内部类对象时无须创建外部类对象。
 * 在外部类以外的地方创建静态内部类实例的语法如下：
 * new OuterClass.InnerConstructor()
 */
class StaticOut {
    // 定义一个静态内部类，不使用访问控制符
    // 即同一个包中的其他类可以访问该内部类
    static class StaticIn {
        public StaticIn() {
            System.out.println("静态内部类的构造器");
        }
    }
}

class CreateStaticInnerInstance {
    public static void test() {
        StaticOut.StaticIn in = new StaticOut.StaticIn();
        // 上面代码可改为下面两行代码
        StaticOut.StaticIn in2;
        in2 = new StaticOut.StaticIn();
    }
}

/**
 * 从上面代码中可以看出，不管是静态内部类还是非静态内部类，它们声明变量的语法完全一样。
 * 区别只是在创建内部类对象时，静态内部类只需使用外部类即可调用构造器，而非静态内部类必须使用外部类对象来调用构造器。
 * <p>
 * 因为调用静态内部类的构造器时无须使用外部类对象，所以创建静态内部类的子类也比较简单，下面代码就为静态内部类StaticIn类定义了一个空的子类。
 * public class StaticSubClass extends StaticOut.StaticIn {}
 * 从上面代码中可以看出，当定义一个静态内部类时，其外部类非常像一个包空间。
 * <p>
 * 注意：相比之下，使用静态内部类比使用非静态内部类要简单很多，只要把外部类当成静态内部类的包空间即可。
 * 因此当程序需要使用内部类时，应该优先考虑使用静态内部类。
 * <p>
 * 既然内部类是外部类的成员，那么是否可以为外部类定义子类，在子类中再定义一个内部类来重写其父类中的内部类呢？
 * 不可以！从上面知识可以看出，内部类的类名不再是简单地由内部类的类名组成，
 * 它实际上还把外部类的类名作为一个命名空间，作为内部类类名的限制。因此子类中的内部类和父类中的内部类不可能完全同名，
 * 即使二者所包含的内部类的类名相同，但因为它们所处的外部类空间不同，所以它们不可能完全同名，也就不可能重写。
 */

/**
 * 局部内部类
 * 如果把一个内部类放在方法里定义，则这个内部类就是一个局部内部类，局部内部类仅在该方法里有效。
 * 由于局部内部类不能在外部类的方法以外的地方使用，因此局部内部类也不能使用访问控制符和static修饰符修饰。
 * 如果需要用局部内部类定义变量、创建实例或派生子类，那么都只能在局部内部类所在的方法内进行。
 */
class LocalInnerClass {     // 对应class文件名 LocalInnerClass.class
    public static void test() {
        class InnerBase {   // 对应class文件名 LocalInnerClass$1InnerBase.class
            int a;
        }
        class InnerSub extends InnerBase { // 对应class文件名 LocalInnerClass$1InnerSub.class
            int b;
        }
        InnerSub is = new InnerSub();
        is.a = 5;
        is.b = 8;
        System.out.println("InnerSub对象的a和b实例变量是：" + is.a + "," + is.b);
    }
}

/**
 * 匿名内部类
 * 匿名内部类适合创建那种只需要一次使用的类，例如前面介绍命令模式时所需要的Command对象。
 * 匿名内部类的语法有点奇怪，创建匿名内部类时会立即创建一个该类的实例，这个类定义立即消失，匿名内部类不能重复使用。
 * 定义匿名内部类的格式如下：
 * <pre>
 *     new 实现接口() | 父类构造器(实参列表)
 *     {
 *         // 匿名内部类的类体部分
 *     }
 * </pre>
 * 从上面定义可以看出，匿名内部类必须继承一个父类，或实现一个接口，但最多只能继承一个父类，或实现一个接口。
 * <p>
 * 关于匿名内部类还有如下两条规则。
 * ➢ 匿名内部类不能是抽象类，因为系统在创建匿名内部类时，会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
 * ➢ 匿名内部类不能定义构造器。由于匿名内部类没有类名，所以无法定义构造器，
 * 但匿名内部类可以定义初始化块，可以通过实例初始化块来完成构造器需要完成的事情。
 * <p>
 * 最常用的创建匿名内部类的方式是需要创建某个接口类型的对象，如下程序所示。
 */
interface Product {
    double getPrice();

    String getName();
}

class AnonymousTest {
    public void buy(Product p) {
        System.out.println("购买一个" + p.getName() + ",花掉了" + p.getPrice());
    }

    public static void test() {
        AnonymousTest ta = new AnonymousTest();
        ta.buy(new Product() {
            @Override
            public double getPrice() {
                return 567.8;
            }

            @Override
            public String getName() {
                return "AGP显卡";
            }
        });
    }
}

/**
 * 当通过实现接口来创建匿名内部类时，匿名内部类不能显式地定义构造器，
 * 因此匿名内部类只有一个隐式的无参数构造器，故new接口名后的括号里不能传入参数值。
 * <p>
 * 但如果通过继承父类来创建匿名内部类时，匿名内部类将拥有和父类相似的构造器，此处的相似指的是拥有相同的形参列表。
 */
abstract class Device {
    private String name;

    public abstract double getPrice();

    public Device() {
    }

    public Device(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class AnonymousInner {
    public void buy(Device d) {
        System.out.println("购买了一个" + d.getName() + ",花掉了" + d.getPrice());
    }

    public static void test() {
        AnonymousInner ai = new AnonymousInner();
        ai.buy(new Device("电子示波器") {
            @Override
            public double getPrice() {
                return 67.8;
            }
        });

        // 此处调用无参数的构造器创建Device匿名实现类对象
        Device d = new Device() {
            // 初始化块
            {
                System.out.println("匿名内部类的初始化块...");
            }

            // 实现抽象方法
            @Override
            public double getPrice() {
                return 56.2;
            }

            // 重写父类的实例方法
            @Override
            public String getName() {
                return "键盘";
            }
        };
        ai.buy(d);
    }
}

/**
 * 当创建匿名内部类时，必须实现接口或抽象父类里的所有抽象方法。如果有需要，也可以重写父类中的普通方法。
 * <p>
 * 在Java 8之前，Java要求被局部内部类、匿名内部类访问的局部变量必须使用final修饰，
 * 从Java 8开始这个限制被取消了，Java 8更加智能：如果局部变量被匿名内部类访问，那么该局部变量相当于自动使用了final修饰。
 * 例如如下程序。
 */
interface A {
    void test();
}

class ATest {
    public static void test() {
        int age = 8;
        A a = new A() {
            @Override
            public void test() {
                // 在java8以前下面编译错误，age必须使用final修饰
                // 从Java8开始，匿名内部类、局部内部类允许访问非final的局部变量
                System.out.println(age);
            }
        };
        a.test();
    }
}

/**
 * 提示：Java 8以后版本的JDK将这个功能称为“effectively final”，
 * 它的意思是对于被匿名内部类访问的局部变量，可以用final修饰，也可以不用final修饰，
 * 但必须按照有final修饰的方式来用—也就是一次赋值后，以后不能重新赋值。
 */

public class InnerClassTest {
    public static void main(String[] args) {
        Cow.test();
        DiscernVariable.test();
        AccessStaticInnerClass.test();
        CreateInnerIntance.test();
        CreateStaticInnerInstance.test();
        LocalInnerClass.test();
        AnonymousTest.test();
        AnonymousInner.test();
        ATest.test();
    }
}
